package com.jsurf;

import com.OpenSURF.OpenSurf;
import edu.cicese.tm.DialogChanges;
import edu.cicese.tm.ImageClusters;
import edu.cicese.tm.Match;
import edu.cicese.tm.Utilities;
import org.json.simple.JSONArray;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import static name.audet.samuel.javacv.jna.cv.CV_BGR2GRAY;
import static name.audet.samuel.javacv.jna.cv.cvCvtColor;
import static name.audet.samuel.javacv.jna.cxcore.*;


public class SurfUtil {
	
	public static Map<String, Points> storedPOIs = new HashMap<String, Points>();
	public static Map<String, Points> storedTags;
	public static String dir, locationDir, poiDir;
	public static String imageForTags/*, poiForTags*/;
	public static String[] imageForLocation;
	
	public static List<InterestPoint> getInterestPoints(String imagePath, float threshold) {
//		Surf.hessian_threshold = threshold;
//		return new Surf(imagePath).getPoints();

		return OpenSurf.getInterestPoints(imagePath, threshold);
	}
	
	public static Map<InterestPoint, InterestPoint> getMatches(List<InterestPoint> ipts1, List<InterestPoint> ipts2) {
		
		Map<InterestPoint, InterestPoint> matchedPoints1 = findMatches(ipts1, ipts2);
		Map<InterestPoint, InterestPoint> matchedPoints2 = findMatches(ipts2, ipts1);

		// take only those points that matched in the reverse comparison too
		Map<InterestPoint, InterestPoint> matchedPoints = new HashMap<InterestPoint, InterestPoint>();
		for (InterestPoint ipt1 : matchedPoints1.keySet()) {
			InterestPoint ipt2 = matchedPoints1.get(ipt1);
			if (ipt1 == matchedPoints2.get(ipt2))
				matchedPoints.put(ipt1, ipt2);
		}
		return matchedPoints;
	}

	//! Finds matching points using the sign of laplacian and a linear nearest neighbor search
	public static Map<InterestPoint, InterestPoint> findMatches(List<InterestPoint> ipts1, List<InterestPoint> ipts2) {
		Map<InterestPoint, InterestPoint> res = new HashMap<InterestPoint, InterestPoint>();
		float distance, bestDistance, secondBest;
		InterestPoint bestMatch;
		int descSize = 64;
		float delta;
		float[] v1, v2;

		for (InterestPoint p1 : ipts1) {
			bestDistance = secondBest = Float.MAX_VALUE;
			bestMatch = null;

			ipts2Loop: for (InterestPoint p2 : ipts2) {
				// Compare descriptors (based on calculating of squared distance between two vectors)
				distance = 0;
				v1 = p1.getDescriptor();
				v2 = p2.getDescriptor();
				for (int i = 0; i < descSize; i++) {
					delta = v1[i] - v2[i];
					distance += delta * delta;
					if (distance >= secondBest)
						continue ipts2Loop;
				}
				if (distance < bestDistance) {
					secondBest = bestDistance;
					bestDistance = distance;
					bestMatch = p2;
				} else { // distance < secondBest
					secondBest = distance;
				}

			}
			
			// Threshold values in other implementations:
			// OpenSURF:                    0.65
			// OpenCV-2.0.0 (find_obj.cpp): 0.6
			// Orig. SURF:                  0.5
			if (bestDistance < 0.65f * secondBest) {
				// Matching point found.
				res.put(p1, bestMatch);
				// Store the change in position (p1 -> p2) into the matchingPoint:
//				bestMatch.setDx(bestMatch.getX() - p1.getX());
//				bestMatch.setDy(bestMatch.getY() - p1.getY());
			}
		}
		return res;
	}

	//! Matches the image (poi file) against the tags (inside a specific spaceID/location)
	//TODO: Include the tag location?
	@SuppressWarnings("unchecked")
	public static String getTags(List<InterestPoint> points, String space) {
		loadTags(space);
		JSONArray jaTags = new JSONArray();
		
		List<Match> matches = new ArrayList<Match>();
		System.out.println("QUERY: begin");
		  	
	  	for (Entry<String, Points> entry : storedTags.entrySet()) {
			Map<InterestPoint, InterestPoint> matchedPoints = getMatches(points, entry.getValue().points);
			matches.add(new Match(matchedPoints.size(), entry.getKey()));
		}
	  	Collections.sort(matches);
	  	
	  	for (int x = 0; x < 5 && x < matches.size(); x++) {
			jaTags.add(matches.get(x).filename);
			System.out.println(matches.get(x).filename + "" + matches.get(x).quantity);
		}
	  	
	  	return jaTags.toJSONString();
	}

	//! Loads all the saved POIs
	public static void loadPOIs() {
		try {
			System.out.println("Loading interest points lists...");
			storedPOIs.clear();
			File dir = new File(poiDir);
			FilenameFilter filter = new FilenameFilter() {
	      		public boolean accept(File dir, String name) {
	          		return name.toLowerCase().endsWith(".poi");
	      		}
			};
			File[] files = dir.listFiles(filter);
			
			for (File f : files) {
				storedPOIs.put(f.getName().substring(0, 3), loadPointsFromFile(f.getCanonicalPath()));
			}
			System.out.println(files.length + " points lists loaded.");
		}
		catch (IOException e) { e.printStackTrace(); }
	}

	//! Loads the saved POIs from a specified spaceID
	public static void loadPOIs(String spaceID) {
		try {
			System.out.println("Loading interest points lists from " + spaceID);
			storedPOIs.clear();
			File dir = new File(Utilities.getPoiDir(spaceID));
			FilenameFilter filter = new FilenameFilter() {
	      		public boolean accept(File dir, String name) {
	          		return name.toLowerCase().endsWith(".poi");
	      		}
			};
			File[] files = dir.listFiles(filter);

			Points points;
			if (files != null) {
				for (File f : files) {
					points = loadPointsFromFile(f.getCanonicalPath());
					if (points.spaceID.equalsIgnoreCase(spaceID)) {
						storedPOIs.put(f.getName().substring(0, 3), points);
					}
				}
			}
			System.out.println(storedPOIs.size() + " points loaded.");
		}
		catch (IOException e) { e.printStackTrace(); }
	}
	
	//! Loads all the tags from a specified directory
	public static void loadTags(String spaceID) {
		try {
			storedTags = new HashMap<String, Points>();
			File dir = new File(Utilities.getTagDir(spaceID) + Utilities.tagSize);
			FileFilter filter = new FileFilter() {
				public boolean accept(File file) {
					return file.isDirectory();
				}
			};
//			FilenameFilter filter = new FilenameFilter() {
//	      		public boolean accept(File dir, String name) {
//	          		return name.toLowerCase().endsWith(".poi");
//	      		}
//			};
			File[] files = dir.listFiles(filter);
			
			for (File f : files) {
				storedTags.put(f.getName(), loadPointsFromFile(f.getCanonicalPath() + "\\" + f.getName() + ".poi"));
			}
		}
		catch (IOException e) { e.printStackTrace(); }
	}

	//TEST
	public static void loadAllTags() {
		try {
			storedTags = new HashMap<String, Points>();
			for (int s = 1; s < 12; s++) {
				File dir = new File(Utilities.getTagDir(Utilities.addZeros(s, 2)) + Utilities.tagSize);
				FileFilter filter = new FileFilter() {
					public boolean accept(File file) {
						return file.isDirectory();
					}
				};

				File[] files = dir.listFiles(filter);
				for (File f : files) {
					storedTags.put(Utilities.addZeros(s, 2) + f.getName(),
							loadPointsFromFile(f.getCanonicalPath() + "\\" + f.getName() + ".poi"));
				}
			}
		}
		catch (IOException e) { e.printStackTrace(); }
	}

	//! Loads the POIs (Points objects) from a file
	public static Points loadPointsFromFile(String filename) {
		try {
			ObjectInputStream str = new ObjectInputStream(new FileInputStream(new File(filename)));
			try {
				return (Points)str.readObject();
			}
			catch (ClassNotFoundException e) { e.printStackTrace(); }
			finally {
				str.close();
			}
		}
		catch (IOException e) { e.printStackTrace(); }
		return null;
	}
	
	//! Creates matching matrix
	@SuppressWarnings({"ResultOfMethodCallIgnored"})
	public static void createMatchingMatrix(String spaceID) {
		loadPOIs(spaceID);
		List<Match> matches;
		Map<String, List<Match>> matchingMatrix = new HashMap<String, List<Match>>();
//		String log = storedPOIs.size() + "\n";
//		File logFile = new File(folder + "\\logs\\matrix.log");
		File matrixDir = new File(Utilities.spaceDir + spaceID);
		matrixDir.mkdirs();

		for (Entry<String, Points> pointsA : SurfUtil.storedPOIs.entrySet()) {
			System.out.println("BEGIN: " + pointsA.getKey());
			matches = new ArrayList<Match>();
//			begin = System.currentTimeMillis();

			for (Entry<String, Points> pointsB : SurfUtil.storedPOIs.entrySet()) {
				if (!pointsA.getKey().equals(pointsB.getKey())) {
					Map<InterestPoint, InterestPoint> matchedPoints =
							SurfUtil.getMatches(pointsA.getValue().points, pointsB.getValue().points);
					matches.add(new Match(matchedPoints.size(), pointsB.getKey()));
				}
			}

			Collections.sort(matches);
			matchingMatrix.put(pointsA.getKey(), matches);

//			end = System.currentTimeMillis();
//			total = end - begin;
//			log += pointsA.getKey() + " ";
//
//			for (int x = 0; x < storedPOIs.size() - 1; x++) {
//				 log += matches.get(x).filename + " " + matches.get(x).quantity + " ";
//			}

//			log += (total / 1000) + "\n";
//		}
//		try {
//			BufferedWriter out = new BufferedWriter(new FileWriter(logFile));
//			out.write(log);
//			out.close();
//		} catch (IOException e) {
//			e.printStackTrace();
//		}

			Utilities.saveToFile(matchingMatrix, matrixDir + "\\matrix.mm");
//			try {
//				ObjectOutputStream stream = new ObjectOutputStream(new FileOutputStream(new File(matrixDir + "\\matrix.mm")));
//				stream.writeObject(matchingMatrix);
//				stream.close();
//			} catch (IOException e) {
//				e.printStackTrace();
//			}
		}
	}

	/**
	 * Loads the matching matrix of a given space from a *.mm file.
	 *
	 * @param spaceID the space from which to load the matching matrix
	 * @return a matching matrix from the space {@code spaceID}, or {@code null}
	 * if no *.mm file exists.
	 */
	@SuppressWarnings("unchecked")
	public static Map<String, List<Match>> loadMatchingMatrix(String spaceID){
		File matrixFile = new File(Utilities.spaceDir + spaceID + "\\matrix.mm");
		if (matrixFile.exists()) {
			try {
				ObjectInputStream str = new ObjectInputStream(new FileInputStream(matrixFile));
				return (Map<String, List<Match>>)str.readObject();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return null;
	}

	/**
	 * Deletes an image from a matching matrix.
	 *
	 * @param mMatrix the matching matrix to update
	 * @param imageID the identifier from the image to delete
	 */
	public static void deleteFromMatchingMatrix(Map<String, List<Match>> mMatrix,
	                                            String imageID) {
		if (mMatrix.containsKey(imageID)) {
			mMatrix.remove(imageID);
			int imageIndex = 0;
			List<Match> matches;
			boolean matchFound;
			for (Entry<String, List<Match>> entry : mMatrix.entrySet()) {
				matches = entry.getValue();
				matchFound = false;
				for (int i = 0; i < matches.size(); i++) {
					if (matches.get(i).filename.equals(imageID)){
						imageIndex = i;
						matchFound = true;
						break;
					}
				}
				if (matchFound) {
				    matches.remove(imageIndex);
				}
			}

			/*// delete from all the image sub-clusters
			ImageClusters imgToRemove = null;
			for (ImageClusters imageClusters : subClusters) {
				if (!imageClusters.id.equals(imageID)) {
					for (Entry<InterestPoint, ArrayList<Match>> cluster : imageClusters.clusters.entrySet()) {
						List<Match> clusterMatches = cluster.getValue();
						List<Integer> thumbIndexes = new ArrayList<Integer>();
						int i;
						for (i = 0; i < clusterMatches.size(); i++) {
//						Match clusterMatch = clusterMatches.get(i);
							if (clusterMatches.get(i).filename.equals(imageID)) {
								thumbIndexes.add(i);
							}
						}
						for (i = 0; !thumbIndexes.isEmpty(); i++) {
							clusterMatches.remove(thumbIndexes.get(0) - i);
							thumbIndexes.remove(0);
						}
					}
				}
				else {
					imgToRemove = imageClusters;
				}
			}
			subClusters.remove(imgToRemove);*/
		}
		else {
			System.out.println("La imagen no fue procesada para calcular la matriz de semejanza.");
		}
	}

	/**
	 * Adds an image to a matching matrix
	 *
	 * @param mMatrix the space matching matrix
	 * @param subClusters a
	 * @param imageID the image identifier
	 * @param poiFile the interest-point file of the image to add
	 * @param dChanges the {@code Dialog} where the process will be displayed
	 */
	public static void addToMatchingMatrix(Map<String, List<Match>> mMatrix,
	                                       String imageID, String poiFile, DialogChanges dChanges) {
		List<Match> matches, newMatches = new ArrayList<Match>();
		Points newEntryPoints = loadPointsFromFile(poiFile);
		storedPOIs.put(imageID, newEntryPoints);

		/*// create image clusters
		ImageClusters imageClusters = new ImageClusters(imageID);
		imageClusters.create(newEntryPoints.points);*/

		double mEntryPerc = dChanges.entryPerc2 / mMatrix.size();
		int mEntryDone = 0;

		// for each entry in the matching matrix (previous added images)
		for (Entry<String, List<Match>> entry : mMatrix.entrySet()) {
			Points entryPoints = storedPOIs.get(entry.getKey());
			Map<InterestPoint, InterestPoint> matchedPoints = getMatches(entryPoints.points, newEntryPoints.points);

			/*// update the sub-clusters
			imageClusters.add(matchedPoints, entry.getKey());*/

			// add the match quantity and update the entry
			matches = entry.getValue();
			matches.add(new Match(matchedPoints.size(), imageID));
			Collections.sort(matches);
			// cache the match of the new image
			newMatches.add(new Match(matchedPoints.size(), entry.getKey()));
			// update the progress bar value
			mEntryDone++;
			dChanges.setValue((int)Math.ceil((mEntryPerc * mEntryDone) + (dChanges.entryPerc1 * 2)));
		}

		// sort and save the image matches
		Collections.sort(newMatches);
		mMatrix.put(imageID, newMatches);

		/*// add the new image sub-clusters
		subClusters.add(imageClusters);*/

//		System.out.println(imageClusters.clusters.size());
//		System.out.println(subClusters.size());
//		System.out.println(subClusters.get(0).clusters.size() + " " + subClusters.get(0).id);

	}

	/**
	 * Compares the interest-points in a selected area from an image
	 * against its best matches.
	 *
	 * @param pointsInArea the list containing the interest-points inside
	 * the area selected
	 * @return a sorted {@code Match} list
	 */
	public static List<Match> getBestMatches(List<InterestPoint> pointsInArea) {
		List<Match> matches = new ArrayList<Match>();
		for (Entry<String, Points> storedPOI : storedPOIs.entrySet()) {
			Map<InterestPoint, InterestPoint> matchedPoints = getMatches(pointsInArea, storedPOI.getValue().points);
			matches.add(new Match(matchedPoints.values(), storedPOI.getKey()));
		}
		Collections.sort(matches);
		return matches;
	}

	/**
	 * Loads the best {@code matchesQuantity} matches interest-points
	 * of an image ({@code imgID}).
	 *
	 * @param spaceID the space where the image is located
	 * @param imgID the image identifier
	 * @param matchingMatrix the space matching matrix
	 * @param matchesQuantity the number of matches to load
	 */
	public static void loadBestMatches(String spaceID, String imgID, Map<String, List<Match>> matchingMatrix,
	                                   int matchesQuantity) {
		try {
			List<Match> bestMatches = new ArrayList<Match>();
			List<Match> allMatches = matchingMatrix.get(imgID);
			for (int x = 0; x < matchesQuantity && x < matchingMatrix.size() - 1; x++) {
				bestMatches.add(allMatches.get(x));
			}

			storedPOIs = new HashMap<String, Points>();
			File dir = new File(Utilities.getPoiDir(spaceID));
			FilenameFilter filter = new FilenameFilter() {
	      		public boolean accept(File dir, String name) {
	          		return name.toLowerCase().endsWith(".poi");
	      		}
			};
			File[] files = dir.listFiles(filter);

			for (File f : files) {
				String img = f.getName().substring(0, 3);
				if (isBestMatch(img, bestMatches)) {
					storedPOIs.put(img, loadPointsFromFile(f.getCanonicalPath()));
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Checks if an image {@code img} is a best match of another image.
	 * @param img the filename of the query image
	 * @param bestMatches the list of images that are best matches
	 * @return {@code True} if {@code img} is a best match and {@code False} if not
	 */
	public static boolean isBestMatch(String img, List<Match> bestMatches) {
		for (Match m : bestMatches) {
			if (img.equals(m.filename)) {
				return true;
			}
		}
		return false;
	}
}